﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using ExpressionEvaluator.BuilderTreeNode;
using System.Reflection;
using ExpressionEvaluator.Expceptions;

namespace ExpressionEvaluator.Extended
{
	internal class ExtendedResolve
	{
		
		private Expression _ParentExpression;
		private BuilderTree _Tree;
		private CodeBlock _FunctionName;
		private List<ParameterForLambdaResolve> _PredictedParameters;
		private bool _UseExtensionsMethod;

		private MethodInfo _RelatedMethod;
        private MethodInfo[] _AmbiguousMethods;
        private List<Type> _ArgumentsTypes = new List<Type>();

		/// <summary>
		/// Initializes a new instance of the <see cref="LambdaResolve"/> class.
		/// </summary>
		private ExtendedResolve(Expression parent, BuilderTree tree, CodeBlock functionName, List<ParameterForLambdaResolve> predictedParameters)
		{
			_ParentExpression = parent;
			_Tree = tree;
			_FunctionName = functionName;
			_PredictedParameters = predictedParameters;
			_UseExtensionsMethod = false;
			_RelatedMethod = null;
		}

		/// <summary>
		/// Locates the object function.
		/// </summary>
		/// <returns>True if one method is located, false if no method is located and Exception if more like one method is located.</returns>
		/// <exception cref="EvalException">If more like one method located.</exception>
		public bool LocateTypeFunction()
		{
			MethodInfo[] methods = _ParentExpression.Type.GetMethods().Where(
				method =>
					// method name match
					(method.Name == _FunctionName.Text)
					// has return type (why to use in expression action lambdas??
					&& (method.ReturnType != null))
				.Where(method => ParameterMatch(method, 0)).ToArray();
			if (methods.Length > 1)
			{
				throw new EvalException(
						string.Format("Ambiguous methods for lambda resolve with name '{0}' and {1} input parameters found.", _FunctionName, _PredictedParameters.Count),
						_FunctionName);
			}
			else if (methods.Length == 1)
			{
				_RelatedMethod = methods[0];
				return true;
			}
			return false;
		}


		/// <summary>
		/// Locates the extension function.
		/// </summary>
		/// <returns></returns>
		private bool LocateExtensionFunction()
		{
            // get type interfaces
			Type[] typeInterfaces = new Type[] { _ParentExpression.Type }
				.Union(_ParentExpression.Type.GetInterfaces())
				.ToArray();
			ExtensionMethodResolver exResolver = new ExtensionMethodResolver(_Tree.Extensions);
			for (int i = 0; i < typeInterfaces.Length; i++)
			{
				string typeInterfaceName = typeInterfaces[i].Name;
				IEnumerable<MethodInfo> exMethods = exResolver.GetExtensionMethodsForType(typeInterfaceName);
				if (exMethods != null)
				{
					var methods = exMethods.Where(
						method =>
							// method name match
					   (method.Name == _FunctionName.Text)
                       && (method.IsGenericMethodDefinition
                            || method.IsGenericMethod 
                            || (method.GetParameters()[0].ParameterType == _ParentExpression.Type))
							// has return type (why to use in expression action lambdas??
					   && (method.ReturnType != null))
						.Where(method => ParameterMatch(method, 1)).ToArray();
                    
					if ((methods.Length > 1)
                        // for more like two generic parameter we need ambiguous resolving
                        || ((methods.Length == 1)
                            && (methods[0].GetGenericArguments().Length > 2)))
					{
                        Type parentType = _ParentExpression.Type;
                        // first add argument type for resolving
                        if (!parentType.IsGenericType)
                        {
                            _ArgumentsTypes.Add(parentType);
                        }
                        else if (parentType.GetGenericTypeDefinition() == typeof(IGrouping<,>))
                        {
                            _ArgumentsTypes.Add(parentType.GetGenericArguments()[1]);
                        }
                        else
                        {
                            foreach (var genericType in parentType.GetGenericArguments())
                            {
                                _ArgumentsTypes.Add(genericType);
                            }
                        }

                        _AmbiguousMethods = methods;
                        _UseExtensionsMethod = true;
                        return true;
                        //throw new EvalException(
                        //        string.Format("Ambiguous extension methods for lambda resolve with name '{0}' and {1} input parameters found.", _FunctionName, _PredictedParameters.Count),
                        //        _FunctionName);
					}
                    else if (methods.Length == 1)
                    {
                        _RelatedMethod = methods[0];
                        _UseExtensionsMethod = true;
                        return true;
                    }
				}
			}
			return false;
		}

		/// <summary>
		/// Parameters the match.
		/// </summary>
		/// <param name="method">The method.</param>
		/// <returns>True if method parameters match with prediction; otherwise false.</returns>
		private bool ParameterMatch(MethodInfo method, int startIndex)
		{
				ParameterInfo[] parameters = method.GetParameters();

				// if parameters count is equal to predicted parameter count
				if (parameters.Length == _PredictedParameters.Count + startIndex)
				{
					bool parameterMatch = true;

					for (int i = startIndex; i < parameters.Length; i++)
					{
						var predictedParam = _PredictedParameters[i - startIndex];

						// if lambda parameter, check if lambda params are equal to predicted lambda parameters (+1 means generic result)
						if (predictedParam.IsLambda)
						{
							int lambdaGenericArgumentLength = parameters[i].ParameterType.GetGenericArguments().Length;
							// if parameter type is lambda expression then get Funct<...> from first generic argument
							if (parameters[i].ParameterType.IsSubclassOf(typeof(LambdaExpression)))
							{
								lambdaGenericArgumentLength = parameters[i].ParameterType.GetGenericArguments()[0].GetGenericArguments().Length;
							}
							if (lambdaGenericArgumentLength != (predictedParam.LambdaParameters.Length + 1))
							{
								parameterMatch = false;
								break;
							}
						}
						// for normal parameters just check if type is equal
						else
						{
							if (parameters[i].ParameterType != predictedParam.NonLambdaParamExpression.Type)
							{
								parameterMatch = false;
								break;
							}
						}
					}

					return parameterMatch;
				}
				return false;
		}

		/// <summary>
		/// Creates the expression.
		/// </summary>
		/// <returns></returns>
		private Expression CreateExpression()
		{
			List<Expression> functionParameters = new List<Expression>();

			// compute function parameters
			for (int pos = 0; pos < _PredictedParameters.Count; pos++)
			{
				var parameter = _PredictedParameters[pos];
				if (!parameter.IsLambda)
				{
					functionParameters.Add(parameter.NonLambdaParamExpression);
				}
				else
				{
					functionParameters.Add(GetLambda(pos, parameter));
				}
			}
			if (_UseExtensionsMethod)
			{
				// because this is extension method add parent to first parameter
				functionParameters.Insert(0, _ParentExpression);

				// find extension interface
				Type extensionInterface = _ParentExpression.Type.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition().Name == _RelatedMethod.GetParameters()[0].ParameterType.Name);
				// if no extension interface, then use parent expression type
				if (extensionInterface == null)
				{
					extensionInterface = _ParentExpression.Type;
				}

				// try resolve generic method
                MethodInfo genericMethod = _RelatedMethod;

                if (_RelatedMethod.IsGenericMethodDefinition)
                {
                    // if just one generic arguments, then no problem
                    if (_RelatedMethod.GetGenericArguments().Length == 1)
                    {
                        genericMethod = _RelatedMethod.MakeGenericMethod(extensionInterface.GetGenericArguments()[0]);
                    }
                    // else do simple generic Arguments prediction
                    else
                    {
                        List<Type> predictedTypes = new List<Type>()
                        {
                            //extensionInterface.GetGenericArguments()[0]
                        };
                        foreach (var param in functionParameters)
                        {
                            Type[] genericArguments = param.Type.GetGenericArguments();
                            foreach (var item in genericArguments)
                            {
                                if (!predictedTypes.Contains(item))
                                {
                                    predictedTypes.Add(item);
                                }
                            }
                        }
                        if (predictedTypes.Count < _RelatedMethod.GetGenericArguments().Length)
                        {
                            predictedTypes.Add(extensionInterface.GetGenericArguments()[0]);
                        }
                        while (predictedTypes.Count > _RelatedMethod.GetGenericArguments().Length)
                        {
                            predictedTypes.RemoveAt(0);
                        }
                        genericMethod = _RelatedMethod.MakeGenericMethod(predictedTypes.ToArray());
                    }
                }

				// create expression
				return Expression.Call(_ParentExpression, genericMethod, functionParameters.ToArray());
			}
			// if normal function
			return Expression.Call(_ParentExpression, _RelatedMethod, functionParameters.ToArray());
		}

		/// <summary>
		/// Gets the lambda.
		/// </summary>
		/// <param name="pos">The pos.</param>
		/// <param name="parameter">The parameter.</param>
		/// <returns>Lambda expression.</returns>
		private Expression GetLambda(int pos, ParameterForLambdaResolve parameter)
		{
			List<ParameterExpression> lambdaParameters = new List<ParameterExpression>();

            // if we have related method then we can use old lambda param prediction
            if ((_RelatedMethod != null) && (_RelatedMethod.GetGenericArguments().Length <= 2))
            {
                lambdaParameters = GetLambdaParametersFromRelatedMethod(pos, parameter);
            }
            // use ambiguous to quest parameter type 
            // - we know, that we are in extension method resolver
            // - we also know, that ambiguous are only generic methos
            else
            {
                foreach (var method in _AmbiguousMethods)
                {
                    Type[] methodGenericArguments = method.GetGenericArguments();
                    Type parameterType = method.GetParameters()[pos + 1].ParameterType;
                    if (parameterType.IsSubclassOf(typeof(LambdaExpression)))
                    {
                        parameterType = parameterType.GetGenericArguments()[0];
                    }
                    Type[] parameterGenerics = parameterType.GetGenericArguments();
                    for (int i = 0; i < parameter.LambdaParameters.Length; i++)
                    {
                        Type resolvedType;
                        // if has generics parameters try to resolve them
                        if (parameterGenerics[i].IsGenericParameter)
                        {
                            int index = FindIndex(methodGenericArguments, parameterGenerics[i]);
                            if (index > _ArgumentsTypes.Count)
                            {
                                throw new EvalException(
                                    string.Format("Ambiguous methods for lambda resolve with name '{0}' and {1} input parameters found.", _FunctionName, _PredictedParameters.Count),
                                    _FunctionName);
                            }
                            resolvedType = _ArgumentsTypes[index];
                        }
                        else
                        {
                            resolvedType = parameterGenerics[i];
                        }
                        // if lambda parameter not yet added
                        if (lambdaParameters.Count == i)
                        {
                            lambdaParameters.Add(Expression.Parameter(resolvedType, parameter.LambdaParameters[i]));
                        }
                        else
                        {
                            // if lambda parameter not match throw exception
                            if (lambdaParameters[i].Type != resolvedType)
                            {
                                throw new EvalException(
                                    string.Format("Ambiguous methods for lambda resolve with name '{0}' and {1} input parameters found.", _FunctionName, _PredictedParameters.Count),
                                    _FunctionName);
                            }
                        }
                    }
                }
            }

			// create lambda tree from existing tree
			BuilderTree lambdaTree = new BuilderTree(
				_Tree.LookupNamespaces, _Tree.LookupAssemblies, _Tree.Extensions,
				_Tree.Parameters.Union(lambdaParameters).ToArray());

			// set lamda tree root to related node (second node, first contains parameters)
			lambdaTree.Root = parameter.LambdaNode.Nodes[1];
            

			// create lambda expression
			Expression result = Expression.Lambda(lambdaTree.GenerateExpression(), lambdaParameters.ToArray());

            // if related method not yet resolved filter existing Ambiguous Methods with expression result
            if (_RelatedMethod == null)
            {
                if (_AmbiguousMethods.Length > 1)
                {
                    _AmbiguousMethods = _AmbiguousMethods.Where(method =>
                        {
                            Type parameterType = method.GetParameters()[pos + 1].ParameterType;
                            if (parameterType.IsSubclassOf(typeof(LambdaExpression)))
                            {
                                parameterType = parameterType.GetGenericArguments()[0];
                                // get result type
                                Type last = parameterType.GetGenericArguments().Last();
                                // if generic parameter then true and resolve result type
                                if (last.IsGenericParameter)
                                {
                                    return true;
                                }
                                return last == result.Type.GetGenericArguments().Last();
                            }
                            if (parameterType.Name.StartsWith("Func`"))
                            {
                                // get result type
                                Type last = parameterType.GetGenericArguments().Last();
                                // if generic parameter then true and resolve result type
                                if (last.IsGenericParameter)
                                {
                                    return true;
                                }
                                return last == result.Type.GetGenericArguments().Last();
                            }
                            return false;
                        }).ToArray();
                }
                // if we have still more ambiguous method, just take first and lets pretend it is ok
                if (_AmbiguousMethods.Length > 0)
                {
                    _RelatedMethod = _AmbiguousMethods[0];
                }
                Type newArgumentType = null;
                foreach (var method in _AmbiguousMethods)
                {
                    Type parameterType = method.GetParameters()[pos + 1].ParameterType;
                    if (parameterType.IsSubclassOf(typeof(LambdaExpression)))
                    {
                        parameterType = parameterType.GetGenericArguments()[0];
                        // get result type
                        Type last = parameterType.GetGenericArguments().Last();
                        Type lastResolved = result.Type.GetGenericArguments().Last();
                        // if generic parameter then true and resolve result type
                        if (last.IsGenericParameter || last.IsGenericType)
                        {
                            if (newArgumentType == null)
                            {
                                newArgumentType = lastResolved;
                            }
                            // if we have different return arguments then throw exception
                            else if (newArgumentType != lastResolved)
                            {
                                throw new EvalException(
                                    string.Format("Ambiguous methods for lambda resolve with name '{0}' and {1} input parameters found.", _FunctionName, _PredictedParameters.Count),
                                    _FunctionName);
                            }
                        }
                    }
                    // for func types
                    else
                    {
                        // get result type
                        Type last = parameterType.GetGenericArguments().Last();
                        Type lastResolved = result.Type.GetGenericArguments().Last();
                        // if generic parameter then true and resolve result type
                        if (last.IsGenericParameter)
                        {
                            newArgumentType = lastResolved;
                        }
                    }
                }
                _ArgumentsTypes.Add(newArgumentType);

            }

            return result;
		}

        private static int FindIndex(Type[] types, Type type)
        {
            for (int i = 0; i < types.Length; i++)
            {
                if (types[i] == type)
                {
                    return i;
                }
            }
            return -1;
        }

        /// <summary>
        /// Gets the lambda parameters from related method.
        /// </summary>
        /// <param name="pos">The pos.</param>
        /// <param name="parameter">The parameter.</param>
        /// <returns>List of lambda parameters.</returns>
        private List<ParameterExpression> GetLambdaParametersFromRelatedMethod(int pos, ParameterForLambdaResolve parameter)
        {
            List<ParameterExpression> lambdaParameters = new List<ParameterExpression>();
            // get genericsArguments (for extensions method add one)
            Type[] genericsArguments = _RelatedMethod.GetParameters()[_UseExtensionsMethod ? pos + 1 : pos].ParameterType.GetGenericArguments();

            // fill lambda parameters
            for (int i = 0; i < parameter.LambdaParameters.Length; i++)
            {
                Type paramType = genericsArguments[i];

                // if extension method and unknown generics argument
                if (_UseExtensionsMethod && (genericsArguments[i].FullName == null))
                {
                    // then locate definition from parent interface
                    Type extensionInterface = _ParentExpression.Type.GetInterfaces().FirstOrDefault(
                        x => x.IsGenericType
                            && x.GetGenericTypeDefinition().Name == _RelatedMethod.GetParameters()[0].ParameterType.Name);

                    // if no extension interface, then use parent expression type
                    if (extensionInterface == null)
                    {
                        extensionInterface = _ParentExpression.Type;
                    }

                    // first argument should be type
                    paramType = extensionInterface.GetGenericArguments()[0];
                }

                // now we can create expression parameter
                lambdaParameters.Add(Expression.Parameter(paramType, parameter.LambdaParameters[i]));
            }
            return lambdaParameters;
        }
		/// <summary>
		/// Creates the expression.
		/// </summary>
		/// <param name="parent">The parent.</param>
		/// <param name="tree">The tree.</param>
		/// <param name="functionName">Name of the function.</param>
		/// <param name="predictedParameters">The predicted parameters.</param>
		internal static Expression CreateExpression(Expression parent, BuilderTree tree, CodeBlock functionName, List<ParameterForLambdaResolve> predictedParameters)
		{
			ExtendedResolve resolver = new ExtendedResolve(parent, tree, functionName, predictedParameters);

			// locate function in type
			if (!resolver.LocateTypeFunction())
			{
				// locate function in extensions methods
				if (!resolver.LocateExtensionFunction())
				{
					throw new EvalException(
								string.Format("Unable to locate method/extensions method for lambda resolve with name '{0}' and {1} input parameters found.", functionName, predictedParameters.Count),
								functionName);
				}
			}
			return resolver.CreateExpression();
		}


	}
}
